Saltar al contenido principal

Introducción a Qiskit

En este cuaderno exploraremos cómo programar puertas cuánticas y circuitos cuánticos con Qiskit, e incluso cómo ejecutarlos en simuladores y computadoras cuánticas reales usando los patrones de Qiskit. Luego presentaremos diferentes formas de codificar información y terminaremos con un ejemplo adicional de Teleportación Cuántica.

Antes de comenzar

Sigue las instrucciones de Instalación y configuración si aún no lo has hecho, incluidos los pasos para Configurar el uso de IBM Quantum™ Platform.

Se recomienda que uses el entorno de desarrollo Jupyter para interactuar con las computadoras cuánticas. Asegúrate de instalar el soporte adicional de visualización recomendado ('qiskit[visualization]'). También necesitarás el paquete matplotlib para la segunda parte de este ejemplo.

Para aprender sobre computación cuántica en general, visita el Curso básico de información cuántica en IBM Quantum Learning

Importaciones

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1

Para ejecutar tus circuitos cuánticos en hardware, primero debes configurar tu cuenta. Puedes hacerlo de la siguiente manera:

  1. Ve a la plataforma IBM Quantum® actualizada.
  2. Ve a la esquina superior derecha (como se muestra en la imagen anterior), crea tu token de API y cópialo en un lugar seguro.
  3. En la siguiente celda, reemplaza deleteThisAndPasteYourAPIKeyHere con tu clave de API.
  4. Ve a la esquina inferior izquierda (como se muestra en la imagen anterior) y crea tu instancia. Asegúrate de elegir el plan abierto.
  5. Una vez creada la instancia, copia su código CRN asociado. Puede que necesites actualizar la página para ver la instancia.
  6. En la celda de abajo, reemplaza deleteThisAndPasteYourCRNHere con tu código CRN.

Consulta esta guía para más detalles sobre cómo configurar tu cuenta de IBM Cloud®.

⚠️ Nota: Trata tu clave de API como tratarías una contraseña segura. Consulta la guía de Configuración en la nube para más información sobre el uso de tu clave de API en entornos seguros y no confiables.

#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"

QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)

1. Puertas Cuánticas y Circuitos Cuánticos

Los circuitos cuánticos son modelos de computación cuántica en los que un cálculo es una secuencia de puertas cuánticas. Veamos algunas de las puertas cuánticas más populares.

Puerta X

Una puerta X equivale a una rotación alrededor del eje X de la esfera de Bloch de π\pi radianes. Mapea 0|0\rangle a 1|1\rangle y 1|1\rangle a 0|0\rangle. Es el equivalente cuántico de la puerta NOT para computadoras clásicas y a veces se denomina bit-flip.

X=(0110)X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix}

# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Puerta H

Una puerta Hadamard representa una rotación de π\pi alrededor del eje que se encuentra en el punto medio del eje XX y el eje ZZ. Mapea el estado base 0|0\rangle a 0+12\frac{|0\rangle + |1\rangle}{\sqrt{2}}, lo que significa que una medición tendrá igual probabilidad de ser 1 o 0, creando una 'superposición' de estados. Este estado también se escribe como +|+\rangle.

H=12(1111)H = \frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \\ \end{pmatrix}

# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Puerta CX (Puerta CNOT)

La puerta NOT controlada (o CNOT o CX) actúa sobre dos Qubits. Realiza la operación NOT (equivalente a aplicar una puerta X) en el segundo Qubit solo cuando el primer Qubit es 1|1\rangle, y de lo contrario lo deja sin cambios. Nota: Qiskit numera los bits en una cadena de derecha a izquierda.

CX=(1000010000010010)CX = \begin{pmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1\\ 0 & 0 & 1 & 0\\ \end{pmatrix}

# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')

Quantum circuit diagram

sv=Statevector(qc)
plot_state_qsphere(sv)

Code output

Crea el primer estado de Bell

ϕ+=12(00+11)|\phi^+ \rangle = \frac{1}{\sqrt 2}(|00 \rangle + |11 \rangle)

# Create a Bell state circuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Code output

Crea el segundo estado de Bell

ϕ=12(0011)|\phi^- \rangle = \frac{1}{\sqrt 2}(|00 \rangle - |11 \rangle)

# Create a circuit with the second Bell state

qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)

qc.draw("mpl")

Quantum circuit diagram

La explicación es que:

H1=12(01)=H|1\rangle=\frac{1}{\sqrt{2} }(|0\rangle-|1\rangle) = |-\rangle
# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crea el estado GHZ de 3 Qubits

GHZ=12(000+111)|GHZ \rangle = \frac{1}{\sqrt 2}(|000 \rangle + |111 \rangle)

# Create a circuit with 3-qubit GHZ state

qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)

qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crea el estado del logo de Qiskit

Qiskit=12(0010+1101)|Qiskit \rangle = \frac{1}{\sqrt 2}(|0010 \rangle + |1101 \rangle)

Centered Image
# Create a circuit with the Qiskit logo state

qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

2. Crea y ejecuta un programa cuántico simple

Los cuatro pasos para escribir un programa cuántico usando los patrones de Qiskit son:

  1. Mapea el problema a un formato nativo cuántico.

  2. Optimiza los circuitos y operadores.

  3. Ejecuta usando una función primitiva cuántica.

  4. Analiza los resultados.

2.1 Mapear el problema a un formato nativo cuántico

En un programa cuántico, los circuitos cuánticos son el formato nativo para representar instrucciones cuánticas, y los operadores representan los observables que se van a medir. Al crear un circuito, normalmente crearás un nuevo objeto QuantumCircuit y luego añadirás instrucciones a él en secuencia.

La siguiente celda de código crea un circuito que produce el estado GHZ, que es un estado en el que tres qubits están completamente entrelazados entre sí.

El SDK de Qiskit utiliza la numeración de bits LSb 0, donde el nthn^{th} dígito tiene el valor 1n1 \ll n o 2n2^n. Para más detalles, consulta el tema Orden de bits en el SDK de Qiskit.

# Create a GHZ state circuit

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

Consulta QuantumCircuit en la documentación para ver todas las operaciones disponibles.

Al crear circuitos cuánticos, también debes considerar qué tipo de datos quieres que se devuelvan tras la ejecución. Qiskit ofrece dos formas de devolver datos: puedes obtener una distribución de probabilidad para un conjunto de qubits que elijas medir, o puedes obtener el valor esperado de un observable. Prepara tu carga de trabajo para medir tu circuito de una de estas dos formas con los primitivos de Qiskit (explicados en detalle en el Paso 3).

Este ejemplo mide valores esperados utilizando el submódulo qiskit.quantum_info, que se especifica mediante operadores (objetos matemáticos utilizados para representar una acción o proceso que cambia un estado cuántico). La siguiente celda de código crea seis operadores de Pauli de tres qubits: ZZZ, ZZX, ZII, XXI, ZZI e III.

# Set up six different observables.

observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]

observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]

Aquí, algo como el operador ZZI es una abreviatura del producto tensorial ZZIZ\otimes Z\otimes I, que significa medir Z en el qubit 2 y Z en el qubit 1 juntos, y obtener información sobre la correlación entre el qubit 2 y el qubit 1. Los valores esperados como este también se escriben típicamente como Z2Z1\langle Z_2 Z_1 \rangle.

Si el estado que observamos es el estado GHZ de tres qubits, entonces la medición de Z2Z1\langle Z_2 Z_1 \rangle debería ser 1.

2.2 Optimizar los circuitos y operadores

Al ejecutar circuitos en un dispositivo, es importante optimizar el conjunto de instrucciones que contiene el circuito y minimizar la profundidad total (aproximadamente el número de instrucciones) del circuito. Esto garantiza que obtengas los mejores resultados posibles al reducir los efectos del error y el ruido. Además, las instrucciones del circuito deben ajustarse a la Arquitectura del Conjunto de Instrucciones (ISA) de un dispositivo backend y deben tener en cuenta las puertas base y la conectividad de qubits del dispositivo.

El siguiente código instancia un dispositivo real al que enviar un trabajo y transforma el circuito y los observables para que coincidan con la ISA de ese backend. Si no has guardado previamente tus credenciales, sigue las instrucciones aquí para autenticarte con tu token de API.

# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)

Transpila el circuito a un circuito ISA

# Convert to an ISA circuit and layout-mapped observables.

pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)

isa_circuit.draw("mpl", idle_wires=False)

Quantum circuit diagram

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]

2.3 Ejecutar usando los primitivos cuánticos

Las computadoras cuánticas pueden producir resultados aleatorios, por lo que normalmente se recopila una muestra de las salidas ejecutando el circuito muchas veces. Puedes estimar el valor del observable utilizando la clase Estimator. Estimator es uno de los dos primitivos; el otro es Sampler, que se puede usar para obtener datos de una computadora cuántica. Estos objetos poseen un método run() que ejecuta la selección de circuitos, observables y parámetros (si aplica), usando un bloque unificado de primitivos (PUB). Al ejecutar este código en hardware cuántico real, considera aplicar técnicas de mitigación y supresión de errores para reducir el ruido intrínseco de la computadora cuántica.

# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000

Envía un trabajo usando el primitivo Estimator.

# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Después de enviar un trabajo, puedes esperar hasta que el trabajo se complete en tu instancia actual de Python, o usar el job_id para recuperar los datos en un momento posterior. (Consulta la sección sobre recuperación de trabajos para más detalles.)

Una vez que el trabajo se completa, examina su salida a través del atributo result() del trabajo.

# This is the result of the entire submission.  You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()

# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]

Ahora también podemos ejecutar el circuito usando el primitivo Sampler

# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Quantum circuit diagram

Envía un trabajo usando el primitivo Sampler.

job_sampler = sampler.run(pm.run([qc]))

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7

2.4 Analizar los resultados

El paso de análisis es típicamente donde podrías posprocesar tus resultados usando, por ejemplo, mitigación de errores de medición o extrapolación de ruido cero (ZNE). Podrías introducir estos resultados en otro flujo de trabajo para un análisis posterior o preparar un gráfico de los valores y datos clave. En general, este paso es específico a tu problema. Para este ejemplo, grafica cada uno de los valores esperados que se midieron para nuestro circuito.

Los valores esperados y las desviaciones estándar para los observables que especificaste a Estimator se acceden a través de los atributos PubResult.data.evs y PubResult.data.stds del resultado del trabajo. Para obtener los resultados de Sampler, usa la función PubResult.data.meas.get_counts(), que devolverá un dict de mediciones en forma de cadenas de bits como claves y recuentos como sus valores correspondientes. Para más información, consulta Comenzar con Sampler.

# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

Plot output

Vemos que los observables ZZIZZI e IIIIII tienen un valor esperado de 1, ya que ZZIZZI introduce dos signos menos que se cancelan, e IIIIII actúa como la identidad, dejando el estado GHZ sin cambios. El resto de los observables tiene un valor esperado de 0, ya que sus operadores ZZ introducen un número impar de signos menos, o los operadores XX voltean un número de qubits que hace que los estados superpuestos sean ortogonales.

Ahora graficamos los resultados para el Sampler

counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

Code output

2.5 Escalar a grandes números de qubits

En la computación cuántica, el trabajo a escala de utilidad es crucial para avanzar en el campo. Dicho trabajo requiere que los cálculos se realicen a una escala mucho mayor; trabajando con circuitos que podrían usar más de 100 qubits y más de 1000 puertas. Este ejemplo da un pequeño paso en esa dirección escalando el problema GHZ a n=10n=10 qubits. Utiliza el flujo de trabajo de patrones de Qiskit y termina midiendo el valor esperado Z0Zi\langle Z_0 Z_i \rangle .

Paso 1. Mapear el problema

Escribe una función que devuelva un QuantumCircuit que prepare un estado GHZ de nn qubits (esencialmente un estado de Bell extendido), luego usa esa función para preparar un estado GHZ de 10 qubits y recopila los observables que se van a medir.

def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:

qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Quantum circuit diagram

A continuación, mapea a los operadores de interés. Este ejemplo usa los operadores ZZ entre qubits para examinar el comportamiento a medida que se alejan entre sí. Los valores esperados cada vez más inexactos (corrompidos) entre qubits distantes revelarían el nivel de ruido presente.

# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))

operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9

Paso 2. Optimizar el problema para la ejecución en el backend cuántico

Transforma el circuito y los observables para que coincidan con la ISA del backend.

# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]

Paso 3. Ejecutar en el backend

Envía el trabajo y, si lo ejecutas en hardware, habilita la supresión de errores usando una técnica para reducir errores llamada desacoplamiento dinámico. El nivel de resiliencia especifica cuánta resiliencia construir contra errores. Los niveles más altos generan resultados más precisos, a expensas de tiempos de procesamiento más largos. Para una explicación más detallada de las opciones establecidas en el siguiente código, consulta Configurar la mitigación de errores para Qiskit Runtime.

# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Paso 4. Posprocesar los resultados

Para comprender mejor el comportamiento de los estados cuánticos entrelazados en hardware real, analizamos las correlaciones por pares entre qubits en la base Z. Específicamente, observamos los valores esperados ⟨Z₀Zᵢ⟩, que miden qué tan fuertemente está correlacionado el qubit 0 con cada otro qubit i. En particular, vamos a graficar:

ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle

¿Qué valores de ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle esperas ver en el gráfico?

Opciones:

a) Decreciente a medida que aumentamos ii

b) Constante en 1

c) Pequeñas desviaciones alrededor de 1

d) Alternando 1 y 0 para valores pares e impares de ii

data = list(range(1, len(operators) + 1))  # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.

plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

Plot output

En este gráfico observamos que Z0Zi\langle Z_0 Z_i \rangle fluctúa alrededor del valor 1, aunque en una simulación ideal todos los Z0Zi\langle Z_0 Z_i \rangle deberían ser 1.

Como puedes ver, los resultados de los experimentos de 10 qubits son buenos pero aún tienen algunos errores. Una forma de mejorar los resultados es implementar el estado GHZ de manera más eficiente.

Normalmente se implementa el estado GHZ con una secuencia de puertas CNOT en forma de escalera. Sin embargo, puedes implementar el estado GHZ de manera más eficiente, reduciendo la profundidad de 2 qubits de n a n/2 o menos.

Una métrica importante para evaluar qué tan precisos serán los resultados, o cuánto ruido tendrá un circuito, es la profundidad de puertas de 2 qubits. Esto se debe a que las tasas de error de las puertas de 2 qubits (~10 veces más altas que las puertas de un solo qubit) dominan los errores de todo el circuito. Usa el siguiente código para obtener la profundidad de puertas de 2 qubits de un circuito.

qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc

better_ghz(n).draw("mpl")

Quantum circuit diagram

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5

Una cosa interesante a destacar aquí es que pudimos reducir la profundidad cuántica del circuito que queremos ejecutar simplemente siendo ingeniosos y pensando en una forma diferente de programarlo. Sin embargo, habrá situaciones y algoritmos en los que no podremos confiar en estos trucos ingeniosos. Aquí es donde el transpilador resulta útil: nos ayuda a optimizar todos estos aspectos de manera eficiente, para que no tengamos que preocuparnos demasiado por ellos.

3. Codificación de Información

3.1 Codificación por amplitud

Ahora que hemos visto cómo construir circuitos cuánticos, es interesante explorar cómo podemos codificar información clásica en estados cuánticos. Un método poderoso es la codificación por amplitud, donde las amplitudes de un estado cuántico representan los componentes de un vector clásico.

Consideremos un ejemplo sencillo. Supongamos que queremos codificar el vector clásico

x=[x0x1x2x3]\vec{x} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ x_3 \end{bmatrix}

en un estado cuántico de dos qubits. El objetivo es preparar el estado cuántico:

ψ=x000+x101+x210+x311\ket{\psi} = x_0\ket{00} + x_1\ket{01} + x_2\ket{10} + x_3\ket{11}

donde x0,x1,x2,x3Rx_0, x_1, x_2, x_3 \in \mathbb{R} (o C\mathbb{C}) y el vector está normalizado de forma que:

x02+x12+x22+x32=1|x_0|^2 + |x_1|^2 + |x_2|^2 + |x_3|^2 = 1

Ahora consideramos el ejemplo particular: x=[0.8924,0.3696,0.2391,0.0990]\vec{x} = [0.8924, 0.3696, 0.2391, 0.0990]

Entonces el estado cuántico correspondiente es:

ψ=0.892400+0.369601+0.239110+0.099011\begin{aligned} \ket{\psi} &= 0.8924\,\ket{00} + 0.3696\,\ket{01} + 0.2391\,\ket{10} + 0.0990\,\ket{11} \end{aligned}

Este estado puede prepararse usando una combinación de puertas de rotación RyR_y de ángulos π/6\pi/6 y π/4\pi/4 para los qubits 0 y 1 respectivamente

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np

qc = QuantumCircuit(2)

qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)

simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()

print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))

Quantum circuit diagram

from qiskit.quantum_info import Statevector

# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)

# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])

# Optional: simulate the state
print("Statevector:", state)

# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Quantum circuit diagram

Así hemos visto cómo codificar información usando puertas de rotación.

3.2 Codificación por ángulo y circuitos parametrizados

Una forma particularmente interesante de codificar información en un ordenador cuántico es diseñar un circuito cuántico que contenga algunos ángulos de rotación θ\vec{\theta} o parámetros que se pueden ajustar para representar una familia de funciones f(θ)f(\vec{\theta}). Consideremos, por ejemplo, el siguiente circuito cuántico parametrizado:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta = Parameter("θ")

qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Matemáticamente, podemos analizar cuál es la familia de funciones que podemos representar con este circuito:

CNOT01Rx{0}(θ)00=CNOT01(cos(θ/2)00isin(θ/2)10)=cos(θ/2)00isin(θ/2)11\text{CNOT}_{01} \, R_x^{\{0\}}(\theta) |00\rangle = \text{CNOT}_{01} \left( \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{10} \right) = \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{11}

Queda bastante claro que el número de estados que podemos representar con este circuito cuántico es limitado, ya que no podemos representar, por ejemplo, los estados 10\ket{10} o 01\ket{01}. Sin embargo, la familia de estados que podemos representar empieza a crecer cuando introducimos más rotaciones en los lugares adecuados:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")

qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

En este caso, los estados cuánticos que representaremos son:

\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}

Podemos ver que este circuito genera una familia más amplia de estados cuánticos en comparación con el anterior. En particular, ahora puede producir estados con amplitudes no nulas para 01\ket{01} o 10\ket{10}, lo que no era posible con el circuito anterior. Sin embargo, este circuito sigue sin ser un generador universal de estados cuánticos, aunque puede ser suficientemente expresivo para diseñar circuitos con cierta flexibilidad para representar determinadas funciones. En general, cuantos más parámetros (ángulos) independientes introduzcamos, mayor expresividad tendrá el circuito para aproximar estados cuánticos arbitrarios.

Ansatzes y biblioteca de circuitos

Este tipo de circuito cuántico parametrizado puede usarse para construir Ansatzes, estados cuánticos de prueba que pretenden aproximar la solución de un problema. Estos Ansatzes son un componente central de los Algoritmos Cuánticos Variacionales, una clase de algoritmos híbridos cuántico-clásicos que emplean un ordenador cuántico para evaluar una función de coste y un optimizador clásico para minimizarla. Entraremos en detalle sobre estos temas en una unidad posterior, pero por ahora presentaremos cómo construir un ansatz sencillo usando la biblioteca de circuitos de Qiskit.

from qiskit.circuit.library import efficient_su2

SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

Quantum circuit diagram

Hemos visto cómo construir un Ansatz sencillo usando la función efficient_su2 de qiskit.circuit.library, que será capaz de generar una amplia gama de estados cuánticos ajustando sus parámetros θ\vec{\theta}.

Conclusión

En este notebook has aprendido a construir circuitos cuánticos, desde la creación de puertas cuánticas hasta la definición y medición de observables, y cómo ejecutar estos circuitos de manera eficiente tanto en simuladores como en hardware cuántico real. También has visto la importancia de un diseño cuidadoso del circuito para minimizar errores al trabajar con dispositivos cuánticos reales, así como estrategias para escalar circuitos a un mayor número de qubits, en particular a través del ejemplo del estado GHZ. Además, has explorado diferentes técnicas para codificar información clásica en estados cuánticos, incluidas la codificación por amplitud y la codificación por ángulo. Con todo esto, estás completamente preparado/a para pasar a la siguiente sesión y comenzar a trabajar con algoritmos cuánticos.

Instalación del Asistente de Código Qiskit en VSCode

Haz clic en el enlace y sigue las instrucciones.

Bonus: Teletransportación Cuántica

Cuando escuchas el término teletransportación cuántica, quizás imagines una tecnología futurista de ciencia ficción que desintegra un objeto en un lugar y lo hace aparecer en otro muy lejano. Pero la teletransportación cuántica no se parece en nada a eso. En realidad, lo que se teletransporta no es materia, sino información.

La teletransportación cuántica permite la transferencia del estado cuántico de un qubit de un lugar a otro. Aunque esta transferencia parece instantánea, no viola las leyes de la física. ¿Cómo es eso posible? ¡Vamos a profundizar en ello!

La teletransportación cuántica es un protocolo que permite a un emisor (Alice) transmitir el estado ψ|\psi\rangle de un qubit q a un receptor (Bob) usando dos recursos clave: un par entrelazado compartido de qubits a y b y dos bits de comunicación clásica c0 y c1.

Esencialmente, lo que el protocolo necesita es:

  • q: el qubit de Alice, inicialmente en el estado ψ|\psi\rangle que queremos teletransportar.
  • a: la mitad de Alice del par entrelazado compartido.
  • b: la mitad de Bob del par entrelazado compartido.
  • c0, c1: bits clásicos para almacenar los resultados de las mediciones de Alice.

¿Y cómo funciona? El flujo de trabajo es el siguiente

  1. Preparar el estado de Alice ψ|\psi\rangle en q. Crearemos un estado específico como +|+\rangle para su verificación.
  2. Crear entrelazamiento: Generar un par de Bell entre a y b.
  3. Operaciones de Alice: Alice realiza una "medición de Bell" sobre sus dos qubits (q y a) y almacena los resultados clásicos en c0 y c1.
  4. Comunicación clásica: Alice envía sus dos bits clásicos (c0, c1) a Bob.
  5. Correcciones de Bob: Bob aplica puertas cuánticas específicas (X y/o Z) a su qubit (b), condicionadas a los valores de c0 y c1 que recibió.

Si todo se hace correctamente, el qubit b de Bob terminará en el estado ψ|\psi\rangle, el estado original del qubit q de Alice.

Para una explicación y exploración más profunda de la teletransportación cuántica, incluyendo la explicación matemática de por qué funciona este protocolo, puedes consultar los recursos de IBM Quantum Learning: Quantum Teleportation. Esto forma parte del curso Basics of Quantum Information.


import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, plot_bloch_multivector

# Define individual quantum registers for each qubit
q = QuantumRegister(1, name='q') # message qubit
a = QuantumRegister(1, name='a') # Alice's entangled qubit
b = QuantumRegister(1, name='b') # Bob's entangled qubit

# Classical register for Alice's measurements
cr_alice = ClassicalRegister(2, name='c_alice')

# Create quantum circuit
teleport_qc = QuantumCircuit(q, a, b, cr_alice, name='Teleportation')

# Step 1: Prepare message state |+⟩ on q
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 2: Create entanglement between a and b
teleport_qc.h(a[0])
teleport_qc.cx(a[0], b[0])
teleport_qc.barrier()

# Step 3: Alice's Bell measurement
teleport_qc.cx(q[0], a[0])
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 4: Alice measures q and a
teleport_qc.measure(q[0], cr_alice[0])
teleport_qc.measure(a[0], cr_alice[1])
teleport_qc.barrier()

# Step 5: Bob's conditional measurements
with teleport_qc.if_test((cr_alice[1], 1)):
teleport_qc.x(b[0])
with teleport_qc.if_test((cr_alice[0], 1)):
teleport_qc.z(b[0])

# Draw the circuit
teleport_qc.draw(output='mpl')

Quantum circuit diagram

Después de ejecutar el protocolo surge una pregunta clave: ¿cómo verificamos que la teletransportación funcionó? No podemos "ver" directamente el estado del qubit de Bob después del protocolo. Sin embargo, dado que preparamos el estado inicial de Alice ψ|\psi\rangle (elegimos +|+\rangle), podemos usar un tipo especial de simulación para comprobar si el qubit b de Bob terminó en ese mismo estado.

Usaremos AerSimulator con save_statevector para comprobar si el qubit b de Bob termina en el estado original de Alice (+|+\rangle). Este simulador calcula el vector de estado cuántico final y luego lo representa usando plot_bloch_multivector para visualizar el qubit de Bob (b) comparado con el estado inicial de Alice (q).

# Simulate the teleportation circuit
sv_simulator = AerSimulator(method='statevector')
teleport_qc_sv = teleport_qc.copy()
teleport_qc_sv.save_statevector()

# Execute the circuit on the statevector simulator
job_sv = sv_simulator.run(teleport_qc_sv)
result_sv = job_sv.result()

# Get the final statevector
final_statevector = result_sv.get_statevector()
print("Visualizing final qubit states:")
display(plot_bloch_multivector(final_statevector))
print("Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.")
Visualizing final qubit states:

Quantum circuit diagram

Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.

Como podemos ver en la visualización, los dos primeros qubits (pertenecientes a Alice) han colapsado a 0 o 1. Mientras tanto, el tercer qubit (perteneciente a Bob), representado en la tercera esfera de Bloch, apunta a lo largo del eje x, lo que indica que se encuentra en el estado +|+\rangle. Así hemos implementado con éxito el protocolo de teletransportación cuántica.

Resumen

En este punto conviene hacer un breve resumen de lo que hemos logrado:

  • Alice ha transmitido un estado cuántico desconocido a Bob.
  • No se ha transferido ninguna partícula física.
  • El estado original en el qubit de Alice se destruye, en consonancia con el teorema de no clonación.